﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Lukianol.Csv.Serialization
{
    /// <summary>
    /// The class is used to serialize objects to the csv format
    /// </summary>
    public sealed class CsvSerializer : ICsvSerializerHeaderProvider
    {

        public const string DefaultColumnDelimiter = ",";
        private readonly Type _collectionType = typeof (IEnumerable);

        public CsvSerializer()
            : this(DefaultColumnDelimiter)
        {
            
        }

        public CsvSerializer(string columnDelimiter) : this(columnDelimiter, null)
        {
            
        }

        public CsvSerializer(ICsvSerializerHeaderProvider headerProvider)
            : this(DefaultColumnDelimiter, headerProvider, CultureInfo.InvariantCulture)
        {

        }

        public CsvSerializer(string columnDelimiter, ICsvSerializerHeaderProvider headerProvider)
            : this(columnDelimiter, headerProvider, CultureInfo.InvariantCulture)
        {
           
        }

        public CsvSerializer(string columnDelimiter, ICsvSerializerHeaderProvider headerProvider, CultureInfo cultureInfo)
        {
             if (columnDelimiter == null)
                 throw new ArgumentNullException("columnDelimiter");

             ColumnDelimiter = columnDelimiter;
             HeaderProvider = headerProvider ?? this;
             SerializationCulture = cultureInfo;
         }

        public string ColumnDelimiter { get; private set; }

        public ICsvSerializerHeaderProvider HeaderProvider { get; private set; }

        public void Serialize(TextWriter textWriter, Object @object)
        {
            if (textWriter == null) 
                throw new ArgumentNullException("textWriter");
            
            var objectType = @object.GetType();
            var isCollection = _collectionType.IsAssignableFrom(objectType);
            var elementType = isCollection ? objectType.GetElementType() ?? objectType : objectType;
            var properties = elementType.GetProperties();
            SerializeHeaderRow(textWriter, properties);
            
            if (isCollection)
                foreach (var element in (IEnumerable)@object)
                    SerializeDataRow(textWriter, properties, element);
            else 
                SerializeDataRow(textWriter, properties, @object);
        }

        public void Serialize(Stream stream, Object @object)
        {
            if (stream == null) 
                throw new ArgumentNullException("stream");
            if (@object == null) 
                throw new ArgumentNullException("object");

            using (var streamWriter = new StreamWriter(stream))
                Serialize(streamWriter, @object);
        }

        public bool CanSerialize(Object @object)
        {
            return @object != null;
        }

        public CultureInfo SerializationCulture { get; private set; }

        private void SerializeRow(TextWriter textWriter, IEnumerable<PropertyInfo> memberInfos, Func<PropertyInfo, object> projection)
        {
            var builder = new StringBuilder();
            var row = memberInfos.Select(projection)
                                    .Aggregate(builder
                                                , (a, b) => a.AppendFormat(SerializationCulture, "\"{1}\"{0}", ColumnDelimiter, b)
                                                , b => b.ToString(0, b.Length - 1));
            textWriter.WriteLine(row);
        }

        private void SerializeHeaderRow(TextWriter textWriter, IEnumerable<PropertyInfo> memberInfos)
        {
            SerializeRow(textWriter, memberInfos, HeaderProvider.GetHeader);
        }

        private void SerializeDataRow(TextWriter textWriter, IEnumerable<PropertyInfo> memberInfos, Object @object)
        {
            SerializeRow(textWriter, memberInfos, m => m.GetValue(@object, null));
        }

        object ICsvSerializerHeaderProvider.GetHeader(PropertyInfo propertyInfo)
        {
            return propertyInfo.Name;
        }
    }
}
